package com.deanlib.cabinet.utils;

import android.text.TextUtils;

import com.alibaba.sdk.android.oss.ClientConfiguration;
import com.alibaba.sdk.android.oss.OSS;
import com.alibaba.sdk.android.oss.OSSClient;
import com.alibaba.sdk.android.oss.common.OSSLog;
import com.alibaba.sdk.android.oss.common.auth.OSSCredentialProvider;
import com.alibaba.sdk.android.oss.common.auth.OSSFederationCredentialProvider;
import com.alibaba.sdk.android.oss.common.auth.OSSFederationToken;
import com.alibaba.sdk.android.oss.model.PutObjectRequest;
import com.alibaba.sdk.android.oss.model.PutObjectResult;

import com.aliyun.sts20150401.Client;
import com.aliyun.sts20150401.models.AssumeRoleRequest;
import com.aliyun.sts20150401.models.AssumeRoleResponse;
import com.aliyun.sts20150401.models.AssumeRoleResponseBody;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.arthenica.ffmpegkit.FFmpegKit;
import com.arthenica.ffmpegkit.FFmpegSession;
import com.arthenica.ffmpegkit.ReturnCode;
import com.deanlib.cabinet.app.Constants;
import com.deanlib.cabinet.base.NetworkAuxiliary;
import com.deanlib.cabinet.data.AppDatabase;
import com.deanlib.cabinet.data.event.ResourceSyncEvent;
import com.deanlib.cabinet.data.model.Key;
import com.deanlib.cabinet.data.model.Resource;
import com.deanlib.cabinet.data.network.NetWorkManager;
import com.deanlib.plantern.Plantern;
import com.deanlib.plantern.utils.FileUtils;
import com.deanlib.plantern.utils.MD5Utils;
import com.elvishew.xlog.XLog;


import org.greenrobot.eventbus.EventBus;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;

/**
 * 上传模块
 * <p>
 * Created by Dean on 2022/7/5
 */
public class OssManager {

    private static OssManager ossManager;

    private static Queue<Task> taskQueue;
    private static Map<String, Task> workMap;
    //    private static Scheduler scheduler;
    private static CompositeDisposable compositeDisposable;
    private static NetworkAuxiliary networkAuxiliary;
    private static OSS ossClient;

    private static final long stsExpiration = 3600L; //单位秒， 取值范围 15分钟-60分钟

    private OssManager() {
        initOSS();
        taskQueue = new ConcurrentLinkedQueue<>();
        workMap = new ConcurrentHashMap<>();
//        scheduler = Schedulers.from(Executors.newFixedThreadPool(5));
        compositeDisposable = new CompositeDisposable();
        networkAuxiliary = new NetworkAuxiliary(null);
    }

    public OSS getOssClient() {
        return ossClient;
    }

    /**
     * 因为有网络请求，不要在主线程操作
     */
    private AssumeRoleResponseBody getOssSts() {
        AssumeRoleResponseBody newKey = null;
        Key key = ConfigManager.getInstance().getOssKey();
        if (key != null) {
            //获取OSS访问授权
            try {
                Client client = new Client(new Config()
                        //todo RAM KEY
//                        .setAccessKeyId(key.appKey)
                        .setAccessKeyId("LTAI5t9pvg24NCKcRE63TzM5")
//                                .setAccessKeySecret(key.appSecret)
                        .setAccessKeySecret("YuVyejGAy4QIPslz0KPxuiIAn9T9lg")
                        .setEndpoint("sts.aliyuncs.com"));
                AssumeRoleRequest assumeRoleRequest = new AssumeRoleRequest()
                        .setDurationSeconds(stsExpiration)
                        .setRoleArn("acs:ram::1105126355043513:role/cabinetossrole") //TODO
                        .setRoleSessionName(UserManager.getInstance().getLogin().user.username);

                AssumeRoleResponse assumeRoleResponse = client.assumeRoleWithOptions(assumeRoleRequest, new RuntimeOptions());
//                        String body = new Gson().toJson(assumeRoleResponse.body);
//                        System.out.println(body);
                newKey = assumeRoleResponse.body;
            } catch (Exception e) {
                e.printStackTrace();
//                        System.err.println(e.getMessage());
            }

        }
        return newKey;
    }

    public void asyncInitOSS() {
        compositeDisposable.add(Observable.create(emitter -> {
            initOSS();
        }).subscribeOn(Schedulers.io()).subscribe());
    }


    public void initOSS() {
        String endpoint = ConfigManager.getInstance().getOssKey().extend1;

        //该配置类如果不设置，会有默认配置，具体可看该类
        ClientConfiguration conf = new ClientConfiguration();
        conf.setConnectionTimeout(15 * 1000); // 连接超时，默认15秒
        conf.setSocketTimeout(15 * 1000); // socket超时，默认15秒
        conf.setMaxConcurrentRequest(5); // 最大并发请求数，默认5个
        conf.setMaxErrorRetry(2); // 失败后最大重试次数，默认2次
        OSSLog.enableLog(); //这个开启会支持写入手机sd卡中的一份日志文件位置在SDCard_path\OSSLog\logs.csv

//        OSSCredentialProvider credentialProvider = new OSSStsTokenCredentialProvider(
//                ConfigManager.getInstance().getOssKey().appKey,
//                ConfigManager.getInstance().getOssKey().appSecret,
//                "<StsToken.SecurityToken>");
        OSSCredentialProvider credentialProvider = new OSSFederationCredentialProvider() {

            @Override
            public OSSFederationToken getFederationToken() {
                // 获取FederationToken，并将其构造为OSSFederationToken对象返回。如果因某种原因FederationToken获取失败，服务器则直接返回null。
                AssumeRoleResponseBody stsKey = getOssSts();
                OSSFederationToken token = null;
                if (stsKey != null && stsKey.credentials != null) {
                    // 从您的服务器中获取Token。
                    token = new OSSFederationToken(
                            stsKey.credentials.accessKeyId,
                            stsKey.credentials.accessKeySecret,
                            stsKey.credentials.securityToken,
                            stsExpiration
                    );
                }
                return token;
            }
        };
//        OSSCredentialProvider credentialProvider = new OSSPlainTextAKSKCredentialProvider(ConfigManager.getInstance().getOssKey().appKey,
//                ConfigManager.getInstance().getOssKey().appSecret);

        ossClient = new OSSClient(Plantern.getAppContext(), endpoint, credentialProvider, conf);
    }

    public static OssManager getInstance() {
        if (ossManager == null) {
            synchronized (OssManager.class) {
                if (ossManager == null) {
                    ossManager = new OssManager();
                }
            }
        }
        return ossManager;
    }

    public synchronized void addTasks(List<Task> tasks) {
        taskQueue.addAll(tasks);
        work();
    }

    public synchronized void addTask(Task task) {
        taskQueue.add(task);
//        recorder.put(task.getMd5(), task);
        work();
    }

    private synchronized void work() {
        if (!ConfigManager.getInstance().isAllowUpload()) {
            XLog.i("Allow upload is false!");
            return;
        }
        if (UserManager.getInstance().isLogin() && ConfigManager.getInstance().enableSync()) {
            XLog.i("OssManager work 开始 taskQueue size:" + taskQueue.size());
            XLog.i("OssManager work 开始 workMap size:" + workMap.size());
            if (workMap.size() >= 6) {
                //清理过期
                workMap.entrySet().removeIf(next -> (System.currentTimeMillis() - next.getValue().getWorkTime()) > Constants.UPLOAD_WORKER_REPEAT_INTERVAL);
            }
            Task task = null;
            while (workMap.size() < 6 && (task = taskQueue.poll()) != null
                    && (System.currentTimeMillis() - task.getCreateTime()) < Constants.UPLOAD_WORKER_REPEAT_INTERVAL) {
                XLog.i("OssManager work while 开始");
                XLog.i("OssManager work while task md5 : " + task.getMd5());
                if ((ConfigManager.getInstance().isSyncImageResource() && FileUtilsExt.isImage(task.getMineType()))
                        || (ConfigManager.getInstance().isSyncVideoResource() && FileUtilsExt.isVideo(task.getMineType()))) {

                    task.setWorkTime(System.currentTimeMillis());
                    workMap.put(task.getMd5(), task);
                    //需要和服务器端通信查询当前资源是否已经上传
                    //接口查询是否存在 md5 资源，
                    // 如果不存在，云端需存储一份不带 path 字段的数据，用于占位，并告诉其他上传用户，该资源正在上传
                    // 如果存在，查看path字段是否为null
                    //   如果path is null ，查询创建日期，如果大于10分钟，可能是上传失败，没有更新 path字段，需要重新上传
                    //   如果path is not null，资源已在云端存在，直接跳过，无需上传
                    Task finalTask = task;
                    networkAuxiliary.request(NetWorkManager.getRequest().doGetResourceOrSave(finalTask.getResource()), optional -> {
                        if (optional.isPresent()) {
                            Resource resource = optional.get();
                            if (TextUtils.isEmpty(resource.path)) {
                                if (System.currentTimeMillis() > (resource.createTime.getTime() + 10 * 60000)) {
                                    //需要重新上传
                                    workThread(finalTask);
                                }
                            } else {
                                workMap.remove(finalTask.getMd5());
                                //不是空的，云端已存在，继续工作，继续下一个任务
                                work();
                                //更新本地资源同步状态
                                Resource dbRes = AppDatabase.getDaoSession().getResourceDao().load(resource.md5);
                                dbRes.synced = true;
                                AppDatabase.getDaoSession().getResourceDao().update(dbRes);
                            }
                        } else {
                            //云端不存在，需要上传
                            workThread(finalTask);
                        }
                    }, err -> {
                        XLog.e(err);
                        err.printStackTrace();
                        workMap.remove(finalTask.getMd5());
                    });
                }
            }
        }
    }

    public static String getCompressDir() {
        return FileUtils.getDiskCachePath() + File.separator + "compress";
    }

    private void workThread(Task task) {
        XLog.i("OssManager WorkThread 开始");
        compositeDisposable.add(Observable.just(task)
                        .map(task1 -> {
                            if (ConfigManager.getInstance().needCompress()) {

                                XLog.i("OssManager WorkThread 准备压缩");
                                String tempPath = getCompressDir()
                                        + File.separator + task1.getMd5() + "_" + System.currentTimeMillis();
                                Map<String, String> compressOptions = new HashMap<>();
                                Resource res = task1.getResource();

                                int limit = 0;
                                if (FileUtilsExt.isImage(task1.getMineType())) {
                                    limit = ConfigManager.getInstance().getCompressImageLimitDimen();
                                    tempPath += ".webp";
                                    task1.setMineType("image/webp");
                                } else if (FileUtilsExt.isVideo(task1.getMineType())) {
                                    limit = ConfigManager.getInstance().getCompressVideoLimitDimen();
                                    tempPath += ".mp4";
                                    task1.setMineType("video/mpeg4");
                                }
                                int width = res.width;
                                int height = res.height;
                                int min = Math.min(res.width, res.height);
                                if (min > limit) {
                                    //需要缩放
                                    int d = Math.max(res.width, res.height) * limit / min;
                                    if (min == res.width) {
                                        width = limit;
                                        height = d;
                                    } else {
                                        width = d;
                                        height = limit;
                                    }
//                            cmd.append(" -s ").append(width).append("x").append(height);
                                    compressOptions.put("-s", width + "x" + height);
                                }

                                if (FileUtilsExt.isVideo(task1.getMineType())) {
                                    if (res.getFrameRate()==null || res.getFrameRate() == 0){
                                        //重新获取

                                    }
                                    //帧率不能超过上限
                                    int frameRate = Math.min(res.getFrameRate(), ConfigManager.getInstance().getCompressFrameRate());
                                    long bitrate = ConfigManager.getInstance().getCompressVideoBitrate(width, height, frameRate);
                                    if (min > limit || res.bitrate > bitrate + 2000) {
//                                cmd.append(" -b:v ").append(bitrate);
                                        compressOptions.put("-b:v", String.valueOf(bitrate));
                                        task1.getResource().bitrate = bitrate;
                                    }
                                    if (frameRate != res.frameRate) {
//                                cmd.append(" -r ").append(frameRate);
                                        compressOptions.put("-r", String.valueOf(frameRate));
                                        task1.getResource().frameRate = frameRate;
                                    }
                                }
                                if (!compressOptions.isEmpty()) {
                                    StringBuilder cmd = new StringBuilder();
                                    cmd.append("-i ").append(task1.getPath());
                                    for (Map.Entry<String, String> option : compressOptions.entrySet()) {
                                        cmd.append(" ").append(option.getKey()).append(" ").append(option.getValue());
                                    }
                                    cmd.append(" ").append(tempPath);
                                    task1.setPath(tempPath);
//                        int rc = FFmpeg.execute("-version");
                                    XLog.i("OssManager WorkThread ffmpeg cmd:" + cmd);
                                    FFmpegSession ffmpegSession = FFmpegKit.execute(cmd.toString());
                                    XLog.i("OssManager WorkThread 压缩结果：" + ffmpegSession.getReturnCode().getValue());
                                    int ffmpegReturnCode = ffmpegSession.getReturnCode().getValue();
                                    if (ffmpegReturnCode == ReturnCode.SUCCESS) {
                                        XLog.i("OssManager WorkThread 压缩成功");
                                        upload(task1);
                                    } else {
                                        XLog.i("OssManager WorkThread 压缩失败 code: " + ffmpegReturnCode);
                                        task1.setResultError("Compress Fail, FFmpeg Return Code:" + ffmpegReturnCode);
                                    }

                                } else {
                                    upload(task1);
                                }

                            } else {
                                upload(task1);
                            }
                            return task1;
                        }).subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(task1 ->
                        {
                            XLog.i("OssManager WorkThread 任务完成 md5：" + task1.getMd5());
                            Task t = workMap.remove(task1.getMd5());
                            XLog.i("OssManager WorkThread task ：" + t);
                            if (t != null && !TextUtils.isEmpty(t.getResultUri()) && t.getResource() != null) {
                                Resource resource = t.getResource();
                                resource.path = t.getResultUri();
                                //这里不再使用保存接口，而是更新接口
                                networkAuxiliary.request(NetWorkManager.getRequest()
                                        .doUpdateResourcePathAndBitrate(t.getMd5(), t.getNewMd5(), resource.path, resource.bitrate), optional -> {
                                    if (optional.isPresent()) {
                                        Resource res = optional.get();
                                        //更新本地同步状态
                                        Resource dbRes = AppDatabase.getDaoSession().getResourceDao().load(resource.md5);
                                        dbRes.newMd5 = res.newMd5;
                                        dbRes.synced = true;
                                        dbRes.bitrate = res.bitrate;
                                        AppDatabase.getDaoSession().getResourceDao().update(dbRes);
                                        //更新 图片列表上传与否的标志
                                        EventBus.getDefault().postSticky(new ResourceSyncEvent(dbRes.md5, dbRes));
                                    }
                                });
                            }
                            work();
                        }, err ->
                        {
                            XLog.e("OssManager WorkThread 任务出错");
                            err.printStackTrace();
                            workMap.remove(task.getMd5());
                        })
        );
    }

    private void stop() {
        compositeDisposable.dispose();
        workMap.clear();
    }

    public static String createObjKey(String md5, String extName) {
        //创建5层目录
        char[] chars = md5.substring(0, 5).toCharArray();
        StringBuilder sb = new StringBuilder();
        for (char c : chars) {
            sb.append(c).append("/");
        }
        //以md5值为文件名，带扩展名
        sb.append(md5);
        if (!TextUtils.isEmpty(extName))
            sb.append(extName);
        return sb.toString();
    }

    private void upload(Task task) {
        if (ossClient != null) {
            XLog.i("OssManager WorkThread upload task:" + task);
            if (!TextUtils.isEmpty(task.getPath())
                    && !TextUtils.isEmpty(task.getMineType())
                    && !TextUtils.isEmpty(task.getMd5())
                    && ConfigManager.getInstance().getOssKey() != null) {

                try {
                    String md5Value = MD5Utils.md5(new File(task.getPath()));
                    task.setNewMd5(md5Value);

                    String extName = null;
                    if (task.getMineType().startsWith("image/jp")) {
                        extName = ".jpg";
                    } else if (task.getMineType().startsWith("video/mp")) {
                        extName = ".mp4";
                    } else if (task.getMineType().startsWith("image/webp")) {
                        extName = ".webp";
                    }
                    String key = createObjKey(task.getMd5(), extName);
//                System.out.println("sjdljfjdf:::key>"+key);
                    // 构造上传请求
                    PutObjectRequest put = new PutObjectRequest(
                            ConfigManager.getInstance().getOssKey().extend2, key, task.getPath());

                    PutObjectResult putObjectResult = ossClient.putObject(put);
//                System.out.println(putObjectResult);
                    if (putObjectResult != null && putObjectResult.getStatusCode() == 200) {
                        XLog.i("OssManager WorkThread upload 上传成功");
                        task.setResultUri("http://" + ConfigManager.getInstance().getOssKey().extend2 + "." +
                                ConfigManager.getInstance().getOssKey().extend1 + "/" + key);
                    }

                /*
                // 异步上传时可以设置进度回调
                put.setProgressCallback(new OSSProgressCallback<PutObjectRequest>() {
                    @Override
                    public void onProgress(PutObjectRequest request, long currentSize, long totalSize) {
                        Log.d("PutObject", "currentSize: " + currentSize + " totalSize: " + totalSize);
                    }
                });
                OSSAsyncTask ossAsyncTask = ossClient.asyncPutObject(put, new OSSCompletedCallback<PutObjectRequest, PutObjectResult>() {
                    @Override
                    public void onSuccess(PutObjectRequest request, PutObjectResult result) {
                        Log.d("PutObject", "UploadSuccess");
//                        System.out.println("sjdljfjdf:::1>"+request);
//                        System.out.println("sjdljfjdf:::2>"+result.getServerCallbackReturnBody());

//                        task.setResultUri(); todo
                    }
                    @Override
                    public void onFailure(PutObjectRequest request, ClientException clientExcepion, ServiceException serviceException) {
                        // 请求异常
                        if (clientExcepion != null) {
                            // 本地异常如网络异常等
                            clientExcepion.printStackTrace();
                            task.setResultError(clientExcepion.getMessage());
                        }
                        if (serviceException != null) {
                            // 服务异常
                            Log.e("ErrorCode", serviceException.getErrorCode());
                            Log.e("RequestId", serviceException.getRequestId());
                            Log.e("HostId", serviceException.getHostId());
                            Log.e("RawMessage", serviceException.getRawMessage());
                            task.setResultError(serviceException.getMessage());
                        }
                    }
                });

                // ossAsyncTask.cancel(); // 可以取消任务

                // ossAsyncTask.waitUntilFinished(); // 可以等待直到任务完成
                */
                } catch (Throwable ce) {
                    System.out.println("Caught an ClientException, which means the client encountered "
                            + "a serious internal problem while trying to communicate with OSS, "
                            + "such as not being able to access the network.");
                    System.out.println("Error Message:" + ce.getMessage());
                    task.setResultError(ce.getMessage());
                }
            }
        }
    }

    public static class Task {
        private String path;
        private String mineType;
        private String md5;
        private String newMd5;
        private long createTime;
        private long workTime;

        private Resource resource;

        private String resultUri;
        private String resultError;

        public Task(Resource resource) {
            this.resource = resource;
            this.path = resource.localPath;
            this.md5 = resource.md5;
            this.createTime = System.currentTimeMillis();
        }

        public Resource getResource() {
            return resource;
        }

        public void setResource(Resource resource) {
            this.resource = resource;
        }

        public String getResultUri() {
            return resultUri;
        }

        public void setResultUri(String resultUri) {
            this.resultUri = resultUri;
        }

        public String getResultError() {
            return resultError;
        }

        public void setResultError(String resultError) {
            this.resultError = resultError;
        }

        public String getPath() {
            return path;
        }

        public void setPath(String path) {
            this.path = path;
        }

        public String getMineType() {
            if (mineType == null && path != null) {
                MediaUtilsExt.MediaFileType fileType = MediaUtilsExt.getFileType(path);
                if (fileType != null) {
                    mineType = fileType.mimeType;
                } else {
                    //todo
                }
            }
            return mineType;
        }

        public void setMineType(String mineType) {
            this.mineType = mineType;
        }

        public String getMd5() {
            return md5;
        }

        public void setMd5(String md5) {
            this.md5 = md5;
        }

        public String getNewMd5() {
            return newMd5;
        }

        public void setNewMd5(String newMd5) {
            this.newMd5 = newMd5;
        }

        public long getCreateTime() {
            return createTime;
        }

        public void setCreateTime(long createTime) {
            this.createTime = createTime;
        }

        public long getWorkTime() {
            return workTime;
        }

        public void setWorkTime(long workTime) {
            this.workTime = workTime;
        }

        @Override
        public String toString() {
            return "Task{" +
                    "path='" + path + '\'' +
                    ", mineType='" + mineType + '\'' +
                    ", md5='" + md5 + '\'' +
                    ", createTime=" + createTime +
                    ", workTime=" + workTime +
                    ", resource=" + resource +
                    ", resultUri='" + resultUri + '\'' +
                    ", resultError='" + resultError + '\'' +
                    '}';
        }
    }
}
